/*
 * update.c - cve-check-tool
 *
 * Copyright (C) 2015 Intel Corporation
 *
 * cve-check-tool is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */

#define _GNU_SOURCE
#include <curl/curl.h>
#include <errno.h>
#include <fcntl.h>
#include <gio/gio.h>
#include <openssl/sha.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <utime.h>

#include "cve-check-tool.h"

#include "config.h"
#include "core.h"
#include "cve-db-lock.h"
#include "cve-string.h"
#include "util.h"

#include "update.h"

#define YEAR_START 2002
#define URI_PREFIX "https://static.nvd.nist.gov/feeds/xml/cve"
#include "fetch.h"

#define UPDATE_THRESHOLD 7200
#define UPDATE_DB_MARKER_SUFFIX "cve.update_db"

static const char *get_home_dir(void)
{
        const char *home;

        home = getenv("HOME");
        if (!home) {
                struct passwd *p;

                p = getpwuid(getuid());
                if (p) {
                        home = p->pw_dir;
                        if (home && !*home) {
                                home = NULL;
                        }
                }
        }

        return home;
}

cve_string *get_db_path(const char *path)
{
        const mode_t mode = S_IRWXU | S_IRWXG | S_IRWXO;
        const char *dir;
        autofree(cve_string) *d = NULL;

        if (!path || !*path) {
                const char *home;

                home = get_home_dir();
                if (!home) {
                        return NULL;
                }

                d = cve_string_dup_printf("%s/%s", home, nvd_dir);
                if (!d) {
                        return NULL;
                }

                dir = d->str;
        } else {
                dir = path;
        }

        if (mkdir(dir, mode)) {
                if (errno != EEXIST) {
                        return NULL;
                }

                if (!cve_is_dir(dir)) {
                        return NULL;
                }
        }

        return cve_string_dup_printf("%s/%s", dir, nvd_file);
}

static cve_string *nvdcve_make_fname(int year, const char *fext)
{
        if (year < 0) {
                return cve_string_dup_printf("nvdcve-2.0-Modified.%s", fext);
        } else {
                return cve_string_dup_printf("nvdcve-2.0-%d.%s", year, fext);
        }
}

static char *nvdcve_meta_get_val(FILE *f, const char *field)
{
        do {
                char field_name[256], field_value[256];
                int ret;

                ret = fscanf(f, " %255[^: \f\n\r\t\v] :%255s", field_name, field_value);
                if (ret != 2) {
                        if (ret != EOF) {
                                continue;
                        }
                        if (ferror(f)) {
                                if (errno == EINTR || errno == EAGAIN) {
                                        clearerr(f);
                                        continue;
                                }
                        }
                        return NULL;
                }
                if (streq(field_name, field)) {
                        return strdup(field_value);
                }
        } while (1);
}

static bool nvdcve_data_ok(const char *meta, const char *data)
{
        autofree(char) *csum_meta = NULL;
        char csum_data[SHA256_DIGEST_LENGTH * 2 + 1];
        unsigned char digest[SHA256_DIGEST_LENGTH];
        struct stat st;
        int fdata;
        void *buffer;
        size_t length;
        FILE *fmeta;
        bool ret = false;

        /* Get digest from the NVD META file */
        fmeta = fopen(meta, "r");
        if (!fmeta) {
                goto err_out;
        }

        csum_meta = nvdcve_meta_get_val(fmeta, "sha256");
        if (!csum_meta) {
                goto err_fclose;
        }

        /* Compute digest on NVD XML file */
        fdata = open(data, O_RDONLY);
        if (fdata < 0) {
                goto err_fclose;
        }

        memset(&st, 0, sizeof(st));
        if (fstat(fdata, &st)) {
                goto err_close;
        }
        length = st.st_size;

        buffer = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fdata, 0);
        if (!buffer) {
                goto err_close;
        }

        if (!SHA256(buffer, length, digest)) {
                goto err_unmap;
        }

        for (size_t i = 0; i < sizeof(digest); i++) {
                size_t idx = i * 2;
                size_t len = sizeof(csum_data) - idx;

                snprintf(&csum_data[idx], len, "%02hhx", digest[i]);
        }

        ret = !strcasecmp(csum_meta, csum_data);

err_unmap:
        munmap(buffer, length);
err_close:
        close(fdata);
err_fclose:
        fclose(fmeta);
err_out:
        return ret;
}

static bool __update_required(const char *db_file, const char *update_fname)
{
        struct stat st;
        time_t t;

        memset(&st, 0, sizeof(st));
        if (stat(db_file, &st)) {
                goto end;
        }

        if (!st.st_size) {
                goto unlink;
        }

        t = time(NULL);
        if (difftime(t, st.st_mtime) >= UPDATE_THRESHOLD) {
                goto end;
        }

        memset(&st, 0, sizeof(st));
        if (!stat(update_fname, &st)) {
                goto unlink;
        }

        return false;
unlink:
        /* Database partial load: unlink it to load again */
        unlink(db_file);
end:
        return true;
}

int update_required(const char *db_file)
{
        autofree(cve_string) *u_fname = NULL;

        u_fname = make_db_dot_fname(db_file, UPDATE_DB_MARKER_SUFFIX);
        if (!u_fname) {
                return -1;
        }

        return __update_required(db_file, u_fname->str);
}

#ifndef O_NOFOLLOW
#define O_NOFOLLOW 0
#endif

static inline int update_begin(const char *update_fname)
{
        const int flags = O_RDONLY | O_CREAT | O_NONBLOCK | O_NOFOLLOW;
        const mode_t mode = S_IRUSR | S_IWUSR;

        return open(update_fname, flags, mode);
}

static inline void update_end(int fd, const char *update_fname, bool ok)
{
        close(fd);
        if (ok) {
                unlink(update_fname);
        }
}

static int do_fetch_update(int year, const char *db_dir, CveDB *cve_db, bool db_exist, bool verbose,
                           const char *cacert_file)
{
        const char nvd_uri[] = URI_PREFIX;
        autofree(cve_string) *uri_meta = NULL;
        autofree(cve_string) *uri_data_gz = NULL;
        autofree(cve_string) *nvdcve_meta = NULL;
        autofree(cve_string) *nvdcve_data = NULL;
        autofree(cve_string) *nvdcve_data_gz = NULL;
        autofree(cve_string) *nvd_xml = NULL;
        autofree(cve_string) *nvd_xml_gz = NULL;
        autofree(cve_string) *nvd_meta = NULL;
        FetchStatus st;
        bool update, load, refetched = false;

        /* Prepare NVD META file/uri pathes */
        nvd_meta = nvdcve_make_fname(year, "meta");
        if (!nvd_meta) {
                return ENOMEM;
        }

        nvdcve_meta = cve_string_dup_printf("%s/%s", db_dir, nvd_meta->str);
        if (!nvdcve_meta) {
                return ENOMEM;
        }

        uri_meta = cve_string_dup_printf("%s/%s", nvd_uri, nvd_meta->str);
        if (!uri_meta) {
                return ENOMEM;
        }

        /* Prepare NVD XML file/uri pathes */
        nvd_xml = nvdcve_make_fname(year, "xml");
        if (!nvd_xml) {
                return ENOMEM;
        }

        nvdcve_data = cve_string_dup_printf("%s/%s", db_dir, nvd_xml->str);
        if (!nvdcve_data) {
                return ENOMEM;
        }

        nvd_xml_gz = cve_string_dup_printf("%s.gz", nvd_xml->str);
        if (!nvd_xml_gz) {
                return ENOMEM;
        }

        nvdcve_data_gz = cve_string_dup_printf("%s/%s", db_dir, nvd_xml_gz->str);
        if (!nvdcve_data_gz) {
                return ENOMEM;
        }

        uri_data_gz = cve_string_dup_printf("%s/%s", nvd_uri, nvd_xml_gz->str);
        if (!uri_data_gz) {
                return ENOMEM;
        }

refetch:
        if (refetched) {
                unlink(nvdcve_meta->str);
                unlink(nvdcve_data->str);
                unlink(nvdcve_data_gz->str);
        }

        /* Fetch NVD META file */
        st = fetch_uri(uri_meta->str, nvdcve_meta->str, verbose, cacert_file);
        if (st == FETCH_STATUS_FAIL) {
                fprintf(stderr, "Failed to fetch %s\n", uri_meta->str);
                return -1;
        }

        /* Fetch NVD XML file */
        st = fetch_uri(uri_data_gz->str, nvdcve_data_gz->str, verbose, cacert_file);
        switch (st) {
        case FETCH_STATUS_FAIL:
                fprintf(stderr, "Failed to fetch %s\n", uri_data_gz->str);
                return -1;
        case FETCH_STATUS_UPDATE:
                update = load = true;
                break;
        default:
                update = !nvdcve_data_ok(nvdcve_meta->str, nvdcve_data->str);
                load = !db_exist;
                break;
        }

        if (update) {
                if (!gunzip_file(nvdcve_data_gz->str)) {
                        if (!refetched) {
                                refetched = true;
                                goto refetch;
                        }
                        fprintf(stderr, "Unable to extract %s\n", nvdcve_data_gz->str);
                        return -1;
                }
                if (!nvdcve_data_ok(nvdcve_meta->str, nvdcve_data->str)) {
                        if (!refetched) {
                                refetched = true;
                                goto refetch;
                        }
                        fprintf(stderr, "Unpacked data %s is not consistent\n", nvdcve_data->str);
                        /* If CVE_SKIP_VERIFY is set in the environment, don't bail here */
                        if (!getenv("CVE_SKIP_VERIFY")) {
                                return -1;
                        }
                }
        }

        if (load) {
                if (!cve_db_load(cve_db, nvdcve_data->str)) {
                        fprintf(stderr, "\nUnable to load: %s\n", nvdcve_data->str);
                        return -1;
                }
        }

        if (verbose) {
                static const char data_report_msg[][sizeof("Skipp")] = {
                            [false] = "Skipp", [true] = "Load",
                };
                fprintf(stderr, "%sed: %s\n", data_report_msg[load], nvd_xml_gz->str);
        }

        return 0;
}

bool update_db(bool quiet, const char *db_file, const char *cacert_file)
{
        autofree(char) *db_dir = NULL;
        autofree(CveDB) *cve_db = NULL;
        autofree(cve_string) *u_fname = NULL;
        struct tm *tm;
        time_t t;
        int u_handle = -1;
        int year;
        bool ret = false;
        bool db_exist = false;
        bool db_locked = false;

        t = time(NULL);
        if (t == (time_t)-1) {
                goto time;
        }

        tm = localtime(&t);
        if (!tm) {
                goto time;
        }

        year = tm->tm_year + 1900;

        u_fname = make_db_dot_fname(db_file, UPDATE_DB_MARKER_SUFFIX);
        if (!u_fname) {
                goto oom;
        }

        printf("update_db: u_fname = %s\n",u_fname->str);
        db_dir = cve_get_file_parent(db_file);
        if (!db_dir) {
                goto oom;
        }

        db_locked = cve_db_write_lock(LOCK_WAIT_SECS);
        if (!db_locked) {
                fputs("Exiting...\n", stderr);
                goto end;
        }

        /* Lock aquired, check if database is still needs update */
        if (!__update_required(db_file, u_fname->str)) {
                ret = true;
                goto end;
        }

        u_handle = update_begin(u_fname->str);
        if (u_handle < 0) {
                fprintf(stderr, "Can't create timestamp file %s\n", u_fname->str);
                goto end;
        }

        db_exist = cve_file_exists(db_file);

        cve_db = cve_db_new(db_file);
        if (!cve_db) {
                fprintf(stderr, "main(): DB initialisation issue\n");
                goto end;
        }

        if (!cve_db_begin(cve_db)) {
                fprintf(stderr, "Failed to initialise DB\n");
                goto end;
        }

        for (int i = YEAR_START; i <= year + 1; i++) {
                int y = i > year ? -1 : i;
                int rc;

                rc = do_fetch_update(y, db_dir, cve_db, db_exist, !quiet, cacert_file);
                switch (rc) {
                case 0:
                        continue;
                case ENOMEM:
                        goto oom;
                default:
                        goto end;
                }
        }

        if (!cve_db_finalize(cve_db)) {
                fprintf(stderr, "Failed to finalize DB\n");
                goto end;
        }

        /* Make sure we always update access and modify time on
         * database file, even if no new data loaded.
         */
        if (utime(db_file, NULL)) {
                fprintf(stderr, "Unable to update file access and modify time\n");
                goto end;
        }

        ret = true;
end:
        if (u_handle >= 0) {
                update_end(u_handle, u_fname->str, ret);
        }
        if (db_locked) {
                cve_db_unlock();
        }
        return ret;
oom:
        fputs("update_db(): Out of memory\n", stderr);
        goto end;
time:
        fputs("Can't get local time\n", stderr);
        goto end;
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 8
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * vi: set shiftwidth=8 tabstop=8 expandtab:
 * :indentSize=8:tabSize=8:noTabs=true:
 */
